<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="UC4G0" id="UC4G0"><span data-lake-id="ud86fe7e3" id="ud86fe7e3">典型回答</span></h1>
  <p data-lake-id="u168b9d26" id="u168b9d26"><span data-lake-id="uf1cd044e" id="uf1cd044e">首先，我们先来梳理下，JVM是如何给对象分配内存的：</span></p>
  <ol list="uc8f677fe">
   <li fid="u72b91b4e" data-lake-id="u545cc944" id="u545cc944"><span data-lake-id="u0d1b327a" id="u0d1b327a">如果JIT的逃逸分析后该对象没有逃逸，那么可能优化到栈上分配。</span></li>
   <li fid="u72b91b4e" data-lake-id="u2b4e0df4" id="u2b4e0df4"><span data-lake-id="u1a9e9384" id="u1a9e9384">否则对象主要分配到新生代上，如果启动了TLAB，则分配到TLAB中。</span></li>
   <li fid="u72b91b4e" data-lake-id="ued463096" id="ued463096"><span data-lake-id="u1bc0cef6" id="u1bc0cef6">如果被判断为大对象，则直接分配到直接进入老年代，譬如很长的字符串和数组，避免为大对象分配内存时由于分配担保机制带来的复制而降低效率 。可以设置</span><code data-lake-id="u88cb54dd" id="u88cb54dd"><span data-lake-id="u3e680e46" id="u3e680e46">-XX:PretenureSizeThreshold</span></code><span data-lake-id="ub85c5d46" id="ub85c5d46">，令大于该尺寸的对象直接进入老年代</span></li>
  </ol>
  <p data-lake-id="u83553609" id="u83553609"><span data-lake-id="uac04e473" id="uac04e473">简而言之，如下图所示：</span></p>
  <p data-lake-id="u895341b2" id="u895341b2"><span data-lake-id="u8c225b18" id="u8c225b18">​</span><br></p>
  <p data-lake-id="u0c332dda" id="u0c332dda"><img src="https://cdn.nlark.com/yuque/0/2022/png/5378072/1671866089292-002857d1-4248-46bc-8710-2f17e0c5877e.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_60%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="ub6f3f766" id="ub6f3f766"><span data-lake-id="u75d3ec02" id="u75d3ec02">​</span><br></p>
  <p data-lake-id="u857e4d84" id="u857e4d84"><span data-lake-id="uac1f8131" id="uac1f8131">所以，我们到这里就很清楚了，当给对象分配内存的时候，有可能在栈上分配，这自然不存在线程安全问题。除此之外，如果在堆上分配，则可能会启动TLAB机制，使得堆内存给线程单独划分空间，避免了线程安全的问题。</span></p>
  <p data-lake-id="u0ca3d167" id="u0ca3d167"><span data-lake-id="u2b971321" id="u2b971321">​</span><br></p>
  <p data-lake-id="u0cbe763f" id="u0cbe763f"><span data-lake-id="u032b4d6e" id="u032b4d6e">同时，当不启动TLAB机制的时候，如果一个空间被多个线程同时分配对象，JVM会采用CAS+失败重试的方式来避免线程问题。（具体的CAS机制和其利弊可以移步到JAVA并发专栏）</span></p>
  <p data-lake-id="u9488a29c" id="u9488a29c"><span data-lake-id="ufdba7d62" id="ufdba7d62">​</span><br></p>
  <p data-lake-id="ua0ad30d1" id="ua0ad30d1"><span data-lake-id="u1f4eae26" id="u1f4eae26">简而言之，是采用乐观锁的方式，只有假定该堆没有被其他线程操作的时候，当前线程才会在堆上分配对象，如果被其他线程操作，就获取当前堆中的最新标识，然后重试</span></p>
  <h1 data-lake-id="i9MSF" id="i9MSF"><span data-lake-id="u5a58f59f" id="u5a58f59f">知识扩展</span></h1>
  <h2 data-lake-id="wME6H" id="wME6H"><span data-lake-id="u61bb4a37" id="u61bb4a37">什么是TLAB</span></h2>
  <p data-lake-id="u7ed0fc13" id="u7ed0fc13"><span data-lake-id="u67e061c2" id="u67e061c2">TLAB是虚拟机在堆内存的eden划分出来的一块专用空间，是线程专属的。在虚拟机的TLAB功能启动的情况下，在线程初始化时，虚拟机会为每个线程分配一块TLAB空间，只给当前线程使用，这样每个线程都单独拥有一个空间，如果需要分配内存，就在自己的空间上分配，这样就不存在竞争的情况，可以大大提升分配效率。</span></p>
  <p data-lake-id="u5e0a7933" id="u5e0a7933"><span data-lake-id="u645fcdce" id="u645fcdce">​</span><br></p>
  <p data-lake-id="ucbf63d1a" id="ucbf63d1a"><span data-lake-id="ud4553085" id="ud4553085">注意到上面的描述中”线程专属”、”只给当前线程使用”、”每个线程单独拥有”的描述了吗？</span></p>
  <p data-lake-id="u8b71191a" id="u8b71191a"><span data-lake-id="u3356b471" id="u3356b471">​</span><br></p>
  <p data-lake-id="u644ced9a" id="u644ced9a"><span data-lake-id="u0f0d864d" id="u0f0d864d">所以说，因为有了TLAB技术，堆内存并不是完完全全的线程共享，其eden区域中还是有一部分空间是分配给线程独享的。</span></p>
  <p data-lake-id="uac0806fa" id="uac0806fa"><span data-lake-id="u0dac257f" id="u0dac257f">​</span><br></p>
  <p data-lake-id="ucfe8307c" id="ucfe8307c"><span data-lake-id="u38c54184" id="u38c54184">这里值得注意的是，我们说TLAB是线程独享的，但是只是在“分配”这个动作上是线程独占的，至于在读取、垃圾回收等动作上都是线程共享的。而且在使用上也没有什么区别。</span></p>
  <p data-lake-id="ud9830727" id="ud9830727"><span data-lake-id="uef2617e2" id="uef2617e2">​</span><br></p>
  <p data-lake-id="u07de307f" id="u07de307f"><span data-lake-id="u063e6578" id="u063e6578">也就是说，虽然每个线程在初始化时都会去堆内存中申请一块TLAB，并不是说这个TLAB区域的内存其他线程就完全无法访问了，其他线程的读取还是可以的，只不过无法在这个区域中分配内存而已。</span></p>
  <p data-lake-id="ua88e3d00" id="ua88e3d00"><span data-lake-id="uf2df6457" id="uf2df6457">​</span><br></p>
  <p data-lake-id="uf586ce01" id="uf586ce01"><span data-lake-id="u0a6b6e52" id="u0a6b6e52">并且，在TLAB分配之后，并不影响对象的移动和回收，也就是说，虽然对象刚开始可能通过TLAB分配内存，存放在Eden区，但是还是会被垃圾回收或者被移到Survivor Space、Old Gen等</span></p>
  <h2 data-lake-id="VpsMJ" id="VpsMJ"><span data-lake-id="u33c2339d" id="u33c2339d">TLAB的缺点</span></h2>
  <p data-lake-id="u101d7ddc" id="u101d7ddc"><span data-lake-id="ue4689417" id="ue4689417">虽然在一定程度上，TLAB大大的提升了对象的分配速度，但是TLAB并不是就没有任何问题的。</span></p>
  <p data-lake-id="u3c22fa68" id="u3c22fa68"><span data-lake-id="u0c564273" id="u0c564273">前面我们说过，因为TLAB内存区域并不是很大，所以，有可能会经常出现不够的情况。在《实战Java虚拟机》中有这样一个例子：</span></p>
  <p data-lake-id="u1bc807da" id="u1bc807da"><span data-lake-id="ue28eab69" id="ue28eab69">​</span><br></p>
  <p data-lake-id="u854c27ea" id="u854c27ea"><span data-lake-id="u4a8159e3" id="u4a8159e3">比如一个线程的TLAB空间有100KB，其中已经使用了80KB，当需要再分配一个30KB的对象时，就无法直接在TLAB中分配，遇到这种情况时，有两种处理方案：</span></p>
  <p data-lake-id="u303b4c04" id="u303b4c04"><span data-lake-id="uf092c8a3" id="uf092c8a3">​</span><br></p>
  <ol list="u1036f1bd">
   <li fid="uf1bbca36" data-lake-id="u80fae528" id="u80fae528"><span data-lake-id="u0f1c610f" id="u0f1c610f">直接在堆内存中对该对象进行内存分配。</span></li>
   <li fid="uf1bbca36" data-lake-id="u4caf8e01" id="u4caf8e01"><span data-lake-id="u218f05bf" id="u218f05bf">废弃当前TLAB，重新申请TLAB空间再次进行内存分配。</span></li>
  </ol>
  <p data-lake-id="u08b30692" id="u08b30692"><span data-lake-id="ub9ded96d" id="ub9ded96d">​</span><br></p>
  <p data-lake-id="u7148c70a" id="u7148c70a"><span data-lake-id="u78d99de5" id="u78d99de5">以上两个方案各有利弊，如果采用方案1，那么就可能存在着一种极端情况，就是TLAB只剩下1KB，就会导致后续需要分配的大多数对象都需要在堆内存直接分配。</span></p>
  <p data-lake-id="u05bc64a9" id="u05bc64a9"><span data-lake-id="u393028aa" id="u393028aa">​</span><br></p>
  <p data-lake-id="u611066f9" id="u611066f9"><span data-lake-id="u93f70348" id="u93f70348">如果采用方案2，也有可能存在频繁废弃TLAB，频繁申请TLAB的情况，而我们知道，虽然在TLAB上分配内存是线程独享的，但是TLAB内存自己从堆中划分出来的过程确实可能存在冲突的，所以，TLAB的分配过程其实也是需要并发控制的。而频繁的TLAB分配就失去了使用TLAB的意义。</span></p>
  <p data-lake-id="uee5e6992" id="uee5e6992"><span data-lake-id="u5537cc69" id="u5537cc69">​</span><br></p>
  <p data-lake-id="u69c57d8d" id="u69c57d8d"><span data-lake-id="u6af9a1af" id="u6af9a1af">为了解决这两个方案存在的问题，虚拟机定义了一个refill_waste的值，这个值可以翻译为“最大浪费空间”。</span></p>
  <p data-lake-id="u6d3d115c" id="u6d3d115c"><span data-lake-id="u1e386a6f" id="u1e386a6f">​</span><br></p>
  <p data-lake-id="uad8ffb9e" id="uad8ffb9e"><span data-lake-id="u845ab9d7" id="u845ab9d7">当请求分配的内存大于refill_waste的时候，会选择在堆内存中分配。若小于refill_waste值，则会废弃当前TLAB，重新创建TLAB进行对象内存分配。</span></p>
  <p data-lake-id="u0b39b1d0" id="u0b39b1d0"><span data-lake-id="ub86d77c3" id="ub86d77c3">​</span><br></p>
  <p data-lake-id="ud6daf147" id="ud6daf147"><span data-lake-id="u64c9ed69" id="u64c9ed69">前面的例子中，TLAB总空间100KB，使用了80KB，剩余20KB，如果设置的refill_waste的值为25KB，那么如果新对象的内存大于25KB，则直接堆内存分配，如果小于25KB，则会废弃掉之前的那个TLAB，重新分配一个TLAB空间，给新对象分配内存。</span></p>
  <p data-lake-id="u0a3b252b" id="u0a3b252b"><span data-lake-id="ubd94c834" id="ubd94c834">​</span><br></p>
  <blockquote data-lake-id="uff9cbfec" id="uff9cbfec">
   <p data-lake-id="u260f9915" id="u260f9915"><span data-lake-id="u4b0b9a84" id="u4b0b9a84">​</span><span data-lake-id="uc9f8307c" id="uc9f8307c" class="lake-fontsize-12" style="color: var(--text-primary)">当一个TLAB被填满或者废弃时，原有TLAB中的对象不会被移动或复制到新的TLAB中。在JVM中，一旦对象被分配在堆上，它们通常会保持在原地直到被垃圾回收。所以，当一个TLAB用完时，线程会简单地分配一个新的TLAB，并在新的TLAB上继续对象分配。原有TLAB中的对象将保留在其当前位置，直到它们不再被引用并由垃圾收集器回收。</span></p>
  </blockquote>
  <p data-lake-id="uf3a261f8" id="uf3a261f8"><span data-lake-id="udccb7799" id="udccb7799"><br></span></p>
 </body>
</html>